package fr.umlv.ig.metroView.map;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.swing.JComponent;

import fr.umlv.ig.event.StationEvent;
import fr.umlv.ig.listener.StationAdapter;
import fr.umlv.ig.metroFactory.GraphicFactory;
import fr.umlv.ig.model.Itinerary;
import fr.umlv.ig.model.Line;
import fr.umlv.ig.model.MetroModelImpl;
import fr.umlv.ig.model.Station;

/**
 * The Map which represents the stations. It's a JComponent. The station are
 * draws in its image.
 * 
 * To associate the map to a model, we grid the image. The left corners of each
 * grid contains a list of stations which are in their case.
 * 
 * In this Map you can selected, open or close a station.
 * 
 * 
 * In this view only one station can be selected.
 * 
 * @author jenseric
 * @author william
 * 
 *
 */
@SuppressWarnings("serial")
public class MetroMapStations extends JComponent {
	/**
	 * The {@link MetroModelImpl} used to draw the stations.
	 */
	private final MetroModelImpl metroModel;
	
	/**
	 * the currentSelected Station.
	 */
	private Station currentSelectedStation;
	
	/**
	 * The image where we are drawing the stations.
	 */
	private final BufferedImage mapImage;
	
	/**
	 * The object used to do transform before drawing.
	 */
	private AffineTransform tranform=new AffineTransform();

	/**
	 * The size of a station when the scale is 1.
	 */
	private static int stationSize = 4;
	
	/**
	 * The half size of a station when the scale is 1.
	 */
	private static int stationHalfSize = 2;

	/**
	 * Map of Point of the map associated by a list of Station.
	 */
	private final Map<Point, List<Station>> listCases;
	
	/**
	 * Size of on case in the map.
	 */
	private final int caseWidth;
	/**
	 * Height of on case.
	 */
	private final int caseHeight;
	/**
	 * The current color when we are drawing.
	 */
	private static Color currentColor;
	
	/**
	 * Stroke used for the drawing.
	 */
	private static final Stroke stroke = new BasicStroke((float)2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 10.0f);
	
	/**
	 * Strock black used during the drawing.
	 */
	private static final Stroke strokeBlack = new BasicStroke((float)4f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 10.0f);
	
	/**
	 * The graphique of the component/
	 */
	private final Graphics2D gs;
	
	/**
	 * Used to marked the station which are already drawn..
	 * 
	 */
	private final Set<String> stationsDraw = new HashSet<String>();

	/**
	 * Rectangle used to draw the stations.
	 */
	private final RoundRectangle2D rectStation = new RoundRectangle2D.Double();
	
	/**
	 * Rectangle used to draw the stations. 
	 */
	private final Rectangle rectStation2 = new Rectangle();
	
	/**
	 * The current Scale of the image.
	 */
	private double scale;
	
	/**
	 * The current translate x of the image when scale = 1;
	 */
	private double translateX ;
	
	/**
	 * The current translate y of the image when scale =1;
	 */
	private double translateY;

	
	/**
	 * Initializes a {@link MetroMapStations} with a {@link MetroModelImpl}
	 * @param metroModel the {@link MetroModelImpl} associated by the 
	 * {@link MetroMapStations}.
	 */
	public MetroMapStations(final MetroModelImpl metroModel) {
		this.metroModel = metroModel;
		this.mapImage = new BufferedImage(900, 650, BufferedImage.TYPE_3BYTE_BGR);
		this.listCases = new HashMap<Point, List<Station>>();
		this.caseWidth = mapImage.getWidth()/10;
		this.caseHeight = mapImage.getHeight()/10;
		MetroMapCases.initKeys(listCases, caseWidth, caseHeight);
		MetroMapCases.initValues(metroModel,listCases, stationSize, caseWidth, caseHeight);
		GraphicFactory.initColors();
		this.gs= mapImage.createGraphics();
		gs.setColor(Color.GRAY);
		gs.fillRect(0, 0, mapImage.getWidth(), mapImage.getHeight());

		metroModel.addStationListener(new StationAdapter() {
			@Override
			public void stationChange(StationEvent event) {
				Station station = (Station) event.getSource();
				if(event.getEventType() == StationEvent.STATION_SELECTED) {
					if(station.equals(currentSelectedStation)) {
						return;
					}
					if(currentSelectedStation !=null) {
						metroModel.unselectedStation(currentSelectedStation);
					}
					currentSelectedStation = station;
					drawStationOnClik(event);

					repaint();
				}
				else if(event.getEventType() == StationEvent.STATION_UNSELECTED) {
					if(station.equals(currentSelectedStation)) {
						currentSelectedStation = null;
						
						drawStationOnClik(event);
						repaint();
					}
				}
				
				else {
					if(currentSelectedStation!= null) {
						metroModel.unselected(currentSelectedStation);
					}
					drawStationOnClik(event);
					repaint();
				}
			}
				
		});
	}
	
	/**
	 * Calls when a user click occured.
	 * @param e the {@link MouseEvent} concerned.
	 * @param scale the scale when we enter in the method.
	 * @param translateX the translate in x when the scale is 1 and we enter 
	 *        in the method.
	 * @param translateY the translate in y when the scale is 1 and we enter 
	 *        in the method.
	 * @return the Station clicked or null if the click is not in a station.
	 */
	private Station onClick(MouseEvent e, double scale, double translateX, double translateY) {
		double realTranslateX = (mapImage.getWidth() - mapImage.getWidth()/scale) / 2 -translateX;
		double realTranslateY = (mapImage.getHeight() - mapImage.getHeight()/scale) / 2 - translateY;
		double pointX = e.getX()/scale + realTranslateX ;
		double pointY = e.getY()/scale + realTranslateY ;
		

		Point casePoint = MetroMapCases.getCase((int)pointX, (int)pointY, caseWidth, caseHeight);
		List<Station> listStations = listCases.get(casePoint);
		
		rectStation2.setRect(0 ,0,(int)(stationSize*scale) , (int) (stationSize*scale) );
		for(Station s : listStations) {
			double x = (metroModel.getX(s)-realTranslateX)*scale - stationHalfSize*scale;
			double y = (metroModel.getY(s)-realTranslateY)*scale - stationHalfSize*scale;
			rectStation2.setLocation((int) x, (int)y);

			if(rectStation2.contains(e.getPoint())){
				return s;
			}
		}
		return null;
	}

	
	/**
	 * Calls when a left click occurred over the map.
	 * @param e {@link MouseEvent} concerned by the click.
	 * @param scale the scale when we enter in the method.
	 * @param translateX the translate in x when the scale is 1 and we enter 
	 *        in the method.
	 * @param translateY the translate in y when the scale is 1 and we enter 
	 *        in the method.
	*/
	public void onLeftClick(MouseEvent e, double scale, double translateX, double translateY) {

		Station station = onClick(e, scale, translateX, translateY);

		if(station == null) {
			if( currentSelectedStation!= null) {
				metroModel.unselectedStation(currentSelectedStation);
			}
			return;
		}	
		if(currentSelectedStation!=null) {
			if(currentSelectedStation.equals(station)) {
				return;
			}
			metroModel.unselectedStation(currentSelectedStation);
		}	
		metroModel.selectedStation(station);

	}
	
	/**
	 * Calls when a right click occurred over the map.
	 * @param e {@link MouseEvent} concerned by the click.
	 * @param scale the scale when we enter in the method.
	 * @param translateX the translate in x when the scale is 1 and we enter 
	 *        in the method.
	 * @param translateY the translate in y when the scale is 1 and we enter 
	 *        in the method.
	*/
	public void onRightClick(MouseEvent e, double scale, double translateX, double translateY) {
		Station station = onClick(e, scale, translateX, translateY);
		if(station == null) {
			if( currentSelectedStation!= null) {
				metroModel.unselectedStation(currentSelectedStation);
			}
			return;
		}
		if(!metroModel.isOpened(station)) {
			metroModel.openStation(station);
		}
		else {
			metroModel.closeStation(station);
		}
	}
	
	/**
	 * Calls to scale the imageOf the Map.
	 * @param scale the scale when we enter in the method.
	 * @param translateX the translate in x when the scale is 1 and we enter 
	 *        in the method.
	 * @param translateY the translate in y when the scale is 1 and we enter 
	 *        in the method.
	*/
	public void zoom(MouseWheelEvent e, double scale, double translateX, double translateY) {
		drawMap(scale, translateX, translateY);
	}
	
	/**
	 * Calls to translate the imageOf the Map.
	 * @param scale the scale when we enter in the method.
	 * @param translateX the translate in x when the scale is 1 and we enter 
	 *        in the method.
	 * @param translateY the translate in y when the scale is 1 and we enter 
	 *        in the method.
	*/
	public void translate(double scale, double translationX, double translationY) {
		drawMap(scale, translationX, translationY);
	}

	

	@Override
	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		Graphics2D g2 = (Graphics2D) g;
		g2.drawImage(mapImage,0,0,900,650,null);

	}

	/**
	 * The Method which draw everyStation of a Line. It's a recursive method.
	 * @param station the first Station to draw in the line
	 * @param gs the graphic of the image where we are drawing.
	 * @param stationsDraw the current Station already draw.
	 */
	private void drawStation(Station station, Graphics2D gs, Set<String> stationsDraw) {
		stationsDraw.add(station.getName());
		Map<Station,Station> mapNextStations = metroModel.getMapNextStations(station);
		for(Entry<Station, Station> entryStation : mapNextStations.entrySet()) {
			Station nextStation = entryStation.getValue();
			if(!stationsDraw.contains(nextStation.getName())) {
				gs.setColor(Color.BLACK);
				gs.setStroke(strokeBlack);
				gs.drawLine((metroModel.getX(station)),
						(metroModel.getY(station)),
						(metroModel.getX(nextStation)),
						(metroModel.getY(nextStation)));
				gs.setColor(currentColor);
				gs.setStroke(stroke);
				gs.drawLine((metroModel.getX(station)),
						(metroModel.getY(station)),
						(metroModel.getX(nextStation)),
						(metroModel.getY(nextStation)));
				drawStation(nextStation, gs, stationsDraw);
			}	
		}	
		double x = metroModel.getX(station) - stationHalfSize;
		double y = metroModel.getY(station) - stationHalfSize;

		this.rectStation.setRoundRect(x-1, y-1, stationSize+2, stationSize+2, 2, 2);
		gs.setColor(Color.BLACK);
		gs.fill(rectStation);
		gs.setColor(currentColor);

		this.rectStation.setRoundRect(x, y, stationSize, stationSize, 2, 2);
		
		if(!metroModel.isOpened(station)) {
			gs.setColor(Color.RED);
			gs.fill(rectStation);
			gs.setColor(currentColor);
			return;
			
		}
		else if(metroModel.isSelected(station)) {
			gs.setColor(Color.GREEN);
			gs.fill(rectStation);
			gs.setColor(currentColor);
			return;
		}
		
		gs.fill(rectStation);


	}

	/** Calls to draw the entire map.
	 * @param scale the scale when we enter in the method.
	 * @param translateX the translate in x when the scale is 1 and we enter 
	 *        in the method.
	 * @param translateY the translate in y when the scale is 1 and we enter 
	 *        in the method.
	*/
	public void drawMap(double scale, double translateX, double translateY) {
		this.scale  = scale;
		this.translateX = translateX;
		this.translateY = translateY;
		Graphics2D gs = (Graphics2D) mapImage.getGraphics();
		AffineTransform old = gs.getTransform();
		gs.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

		gs.setBackground(Color.GRAY);
		gs.clearRect(0, 0, getWidth(), getHeight());

		tranform = ((AffineTransform.getTranslateInstance( (double)getWidth() * (1.0 - scale) / 2, (double)getHeight() * (1.0 - scale) / 2)));
		tranform.concatenate(AffineTransform.getScaleInstance( scale , scale));
		tranform.concatenate(AffineTransform.getTranslateInstance(translateX , translateY));

		gs.transform(tranform);

		gs.setStroke(stroke);
		
		int numberOfLine = metroModel.getCountLine();
		for(int i = 0; i<numberOfLine; i++) {
			drawLine(metroModel.getLine(i), gs);
		}
		
		gs.setTransform(old);
		gs.dispose();		
	}
	
	
	/**
	 * Draws in the map when a station state are change.
	 * @param e the {@link StationEvent} the event concerned by the drawing.
	 */
	public void drawStationOnClik(StationEvent e) {
		Graphics2D gs = (Graphics2D) mapImage.getGraphics();
		gs.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

		tranform = ((AffineTransform.getTranslateInstance( (double)getWidth() * (1.0 - scale) / 2, (double)getHeight() * (1.0 - scale) / 2)));
		tranform.concatenate(AffineTransform.getScaleInstance( scale , scale));
		tranform.concatenate(AffineTransform.getTranslateInstance(translateX , translateY));
		
		gs.transform(tranform);
		Station station = (Station) e.getSource();
		double x1 = metroModel.getX(station) - stationHalfSize;
		double y1 = metroModel.getY(station) - stationHalfSize;		
		rectStation.setRoundRect(x1, y1, stationSize, stationSize, 2, 2);
		
		if(e.getEventType() == StationEvent.STATION_SELECTED) {
			gs.setColor(Color.GREEN);
			gs.fill(rectStation);
		}
		
		else if(e.getEventType() == StationEvent.STATION_CLOSED){
			gs.setColor(Color.RED);
			gs.fill(rectStation);
		}
		
		else {
			gs.setColor(GraphicFactory.getLineColor((metroModel.getLine(station).getNumber())));
			gs.fill(rectStation);
		}
		
		
		gs.setColor(currentColor);
		gs.dispose();
	}
	
	
	/**
	 * Draws in the map when a station  is unselected.
	 * @param e the {@link StationEvent} the event concerned by the drawing.
	 */
	public void drawStationUnSelected(int x, int y) {
		Graphics2D gs = (Graphics2D) mapImage.getGraphics();
		gs.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

		tranform = ((AffineTransform.getTranslateInstance( (double)getWidth() * (1.0 - scale) / 2, (double)getHeight() * (1.0 - scale) / 2)));
		tranform.concatenate(AffineTransform.getScaleInstance( scale , scale));
		tranform.concatenate(AffineTransform.getTranslateInstance(translateX , translateY));
		
		gs.transform(tranform);
		double x1 = x - stationHalfSize;
		double y1 = y - stationHalfSize;		
		rectStation.setRoundRect(x1, y1, stationSize, stationSize, 2, 2);
		gs.setColor(Color.GREEN);
		gs.fill(rectStation);
		gs.setColor(currentColor);
		
		gs.dispose();
	}

	
	/**
	 * Draws a entire line with his Stations and connection.
	 * @param line the line to draw.
	 * @param gs the graphic of the image to draw our map.
	 */
	private void drawLine(Line line, Graphics2D gs) {
		int numberOfItinerary = metroModel.getCountItinerary(line);
		for(int i=0; i < numberOfItinerary; i++) {
			Itinerary itinerary = metroModel.getItinerary(line, i);
			gs.setColor(GraphicFactory.getLineColor(line.getNumber()));
			currentColor = gs.getColor();
			drawStation( metroModel.getStart(itinerary), gs, stationsDraw);
		}
		stationsDraw.clear();

	}

}


